
package com.csc440.team2.communication;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.List;


import android.content.Context;
import android.content.Intent;
import android.telephony.SmsManager;
import android.util.Log;


import com.csc440.team2.SmilConstants;
import com.csc440.team2.database.entities.FolderType;
import com.csc440.team2.database.entities.Message;
import com.csc440.team2.smil.SMILAudioComponent;
import com.csc440.team2.smil.SMILComponent;
import com.csc440.team2.smil.SMILImageComponent;
import com.csc440.team2.smil.SMILMessage;
import com.csc440.team2.smil.SMILReadException;
import com.csc440.team2.smil.SMILVideoComponent;


/**
 * Handles the client side of the message server connection to send and receive
 * messages.
 * 
 * @author Jeremy Spitzig
 */
public class ClientHandler implements Runnable
{
    private ConnectionStatus connectionStatus = ConnectionStatus.RECIEVING_SMIL;
    private byte[] input;
    private String key;
    private SMILMessage smilMessage;
    private Message savedMessage;
    private String message;
    private String command;
    private String resourceDetails;
    private int resourceTypeID;
    private String resourceLocation;
    private byte[] resource;
    private String reciever;
    private String sender;
    private boolean finished = false;
    private int port;
    private String serverAddress = "8.8.198.203";// "10.0.2.2";

    MessageAction action;

    Context context;


    /**
     * Gets the context
     * 
     * @return context
     */
    public Context getContext()
    {
        return context;
    }


    /**
     * Sets the context
     * 
     * @param context the context to set
     */
    public void setContext(Context context)
    {
        this.context = context;
    }


    /**
     * Sets the sender
     * 
     * @param sender the sender to set
     */
    public void setSender(String sender)
    {
        this.sender = sender;
    }


    /**
     * Gets the sender
     * 
     * @return the sender
     */
    public String getSender()
    {
        return this.sender;
    }


    /**
     * Sets the SMIL Message to be sent
     * 
     * @param smilMessage the message to be sent
     */
    public void setSMILMessage(SMILMessage smilMessage)
    {
        this.smilMessage = smilMessage;
    }


    /**
     * Gets the SMIL Message
     * 
     * @return the SMIL Message
     */
    public SMILMessage getSMILMessage()
    {
        return this.smilMessage;
    }


    /**
     * Gets the key
     * 
     * @return the key
     */
    public String getKey()
    {
        return key;
    }


    /**
     * Sets the key
     * 
     * @param key the key to set
     */
    public void setKey(String key)
    {
        this.key = key;
    }


    /**
     * Sets the receiver
     * 
     * @param reciever the reciever to set
     */
    public void setReciever(String reciever)
    {
        this.reciever = reciever;
    }


    /**
     * Gets the receiver
     * 
     * @return the receiver
     */
    public String getReciever()
    {
        return this.reciever;
    }


    /**
     * Sets the MessageAction
     * 
     * @param action MessageAction to set
     */
    public void setAction(MessageAction action)
    {
        this.action = action;
    }


    /**
     * Gets the MessageAction
     * 
     * @return the MessageAction
     */
    public MessageAction getAction()
    {
        return this.action;
    }


    /**
     * Gets the server port
     * 
     * @return the server port
     */
    public int getPort()
    {
        return port;
    }


    /**
     * Sets the server port
     * 
     * @param port server port to set
     */
    public void setPort(int port)
    {
        this.port = port;
    }


    /**
     * Sends a stream of bytes over the socket connection
     * 
     * @param connection connection to use when sending the bytes
     * @param data bytes to send
     */
    private void sendBytes(Socket connection, byte[] data)
    {
        int length = data.length;
        String lengthString = Integer.toString(length) + '\n';

        try
        {
            connection.getOutputStream().write(lengthString.getBytes());
            connection.getOutputStream().write(data);
        }
        catch (IOException e)
        {
            Log.e("ClientHandler", "Failed to send bytes", e);
        }
    }


    /**
     * Reads data from the server connection
     * 
     * @param connection server connection to read from
     */
    protected void readInput(Socket connection)
    {
        int inputSize;
        byte inputByte;
        StringBuilder stringBuilder = new StringBuilder();

        try
        {
            // begin reading
            inputByte = (byte) connection.getInputStream().read();

            // read until we see a new line character
            while (inputByte != (byte) '\n')
            {
                stringBuilder.append((char) inputByte);
                inputByte = (byte) connection.getInputStream().read();
            }

            // determine the size of the bytes we just read
            inputSize = Integer.parseInt(stringBuilder.toString());
            input = new byte[inputSize];
            int amountRead = 0;
            int totalRead = 0;

            // read up to the size of our input
            while (totalRead < inputSize)
            {
                amountRead = connection.getInputStream().read(input, totalRead, inputSize - totalRead);
                totalRead += amountRead;
            }
        }
        catch (IOException e)
        {
            Log.e("ClientHandler", "Failed to read bytes", e);
        }
        newline();
    }


    /**
     * Converts a byte array to a String
     * 
     * @param bytes bytes to convert
     * @return String representation of the bytes
     */
    protected String byteArrayToString(byte[] bytes)
    {
        StringBuilder stringBuilder = new StringBuilder();

        // convert each byte to a character and append to the string builder
        for (byte currentByte : bytes)
        {
            stringBuilder.append((char) currentByte);
        }

        return stringBuilder.toString();
    }


    /**
     * Utility method for server communications. Parses the binary data received
     * from the server into meaningful objects
     */
    protected void newline()
    {
        if (action == MessageAction.SEND)
        {
            key = byteArrayToString(input);
            Log.d("ClientHandler", key);
        }
        else if (action == MessageAction.RECIEVE)
        {
            switch (connectionStatus)
            {
                case RECIEVING_SMIL:
                    message = byteArrayToString(input);
                    Log.d("ClientHandler", message);
                    break;
                case RECIEVING_COMMAND:
                    command = byteArrayToString(input);
                    break;
                case RECIEVING_RESOURCE_DETAILS:
                    resourceDetails = byteArrayToString(input);
                    String[] resourceDetailParts = resourceDetails.split("\\|");
                    resourceTypeID = Integer.parseInt(resourceDetailParts[0]);
                    resourceLocation = resourceDetailParts[1];
                    break;
                case RECIEVING_RESOURCE_DATA:
                    resource = input;
                    File resourceFile = new File(resourceLocation);
                    File path = resourceFile.getParentFile();
                    if (path != null)
                    {
                        if (!path.mkdirs())
                        {
                            Log.e("ClientHandler", "Failed to create resource path.");
                        }
                    }
                    FileOutputStream fileOutputStream;
                    try
                    {
                        if (!resourceFile.exists())
                        {
                            if (!resourceFile.createNewFile())
                            {
                                Log.e("ClientHandler", "Failed to create resource file.");
                            }
                        }
                        fileOutputStream = new FileOutputStream(resourceFile);
                        fileOutputStream.write(resource);
                        fileOutputStream.close();
                    }
                    catch (FileNotFoundException e)
                    {
                        Log.e("ClientHandler", "Failed to write resource file", e);
                    }
                    catch (IOException e)
                    {
                        Log.e("ClientHandler", "Failed to write resource file", e);
                    }
                    break;
            }
        }
    }


    /**
     * Prints a debug message to the log
     * 
     * @param message message to print
     */
    protected void printDebugMessage(String message)
    {
        Log.d("ClientHandler", message);
    }


    /**
     * Sends the current message to the message server
     */
    private void send()
    {
        // stream for reading file data
        FileInputStream fileInputStream = null;
        try
        {
            // build the connection to the server and send the initial XML data
            Socket connection = new Socket(serverAddress, port);
            sendBytes(connection, "send".getBytes());
            sendBytes(connection, smilMessage.getXml().getBytes());
            readInput(connection);

            // process each component loaded in the message
            List<SMILComponent> componentList = smilMessage.getComponentList();
            for (SMILComponent smilComponent : componentList)
            {
                // check each component type
                if (smilComponent instanceof SMILImageComponent)
                {
                    // get components meta information
                    int type = ((SMILImageComponent) smilComponent).getType();
                    String source = ((SMILImageComponent) smilComponent).getSrc();
                    File file = new File(source);
                    int fileSize = (int) file.length();
                    byte[] fileBytes = new byte[fileSize];
                    fileInputStream = new FileInputStream(file);
                    fileInputStream.read(fileBytes);

                    // send the component to the server
                    Log.d("ClientHandler", "Transmitting resource");
                    sendBytes(connection, "resource".getBytes());
                    sendBytes(connection, (Integer.toString(type) + "|" + source).getBytes());
                    sendBytes(connection, fileBytes);
                    Log.d("ClientHandler", "Finished transmitting resource");
                }
                else if (smilComponent instanceof SMILAudioComponent)
                {
                    // get components meta information
                    int type = ((SMILAudioComponent) smilComponent).getType();
                    String source = ((SMILAudioComponent) smilComponent).getSrc();
                    File file = new File(source);
                    int fileSize = (int) file.length();
                    byte[] fileBytes = new byte[fileSize];
                    fileInputStream = new FileInputStream(file);
                    fileInputStream.read(fileBytes);

                    fileInputStream.close();
                    // send the component to the server
                    Log.d("ClientHandler", "Transmitting resource");
                    sendBytes(connection, "resource".getBytes());
                    sendBytes(connection, (Integer.toString(type) + "|" + source).getBytes());
                    sendBytes(connection, fileBytes);
                    Log.d("ClientHandler", "Finished transmitting resource");
                }
                else if (smilComponent instanceof SMILVideoComponent)
                {
                    // get components meta information
                    int type = ((SMILVideoComponent) smilComponent).getType();
                    String source = ((SMILVideoComponent) smilComponent).getSrc();
                    File file = new File(source);
                    int fileSize = (int) file.length();
                    byte[] fileBytes = new byte[fileSize];
                    fileInputStream = new FileInputStream(file);
                    fileInputStream.read(fileBytes);
                    fileInputStream.close();

                    // send the component to the server
                    Log.d("ClientHandler", "Transmitting resource");
                    sendBytes(connection, "resource".getBytes());
                    sendBytes(connection, (Integer.toString(type) + "|" + source).getBytes());
                    sendBytes(connection, fileBytes);
                    Log.d("ClientHandler", "Finished transmitting resource");
                }
            }
            sendBytes(connection, "end".getBytes());
            SmsManager manager = SmsManager.getDefault();

            /*
             * send the notification text message to let receiver know there is
             * a message waiting
             * 
             * TODO this is where we could plug in "Push" messaging to eliminate
             * the need for SMS
             */
            if (reciever != null && !"".equals(reciever))
                manager.sendTextMessage(this.reciever, null, key, null, null);
        }
        catch (UnknownHostException e)
        {
            Log.e("ClientHandler", "Could not reach host server", e);
        }
        catch (IOException e)
        {
            Log.e("ClientHandler", "I/O error occured while working with resources", e);
        }
        catch (SMILReadException e)
        {
            Log.e("ClientHandler", "An error occured while reading the SMIL message", e);
        }
        finally
        {
            if (fileInputStream != null)
                try
                {
                    fileInputStream.close();
                }
                catch (IOException e)
                {
                    Log.e("ClientHandler", "Failed to close file stream", e);
                }
        }
    }


    /**
     * Receives the message data from the message server
     */
    private void recieve()
    {
        Intent smilRecievedIntent;
        try
        {
            // build the initial connection to the server
            Socket connection = new Socket(serverAddress, port);
            sendBytes(connection, "recieve".getBytes());
            sendBytes(connection, key.getBytes());
            readInput(connection);

            savedMessage = saveMessage(sender, message);

            while (finished == false)
            {
                connectionStatus = ConnectionStatus.RECIEVING_COMMAND;
                readInput(connection);
                handleCommand(connection);
            }

            // fire the intent that will notify the user of a new SMIL message
            smilRecievedIntent = new Intent("com.csc440.team2.intents.SMILRecieved");
            smilRecievedIntent.putExtra(SmilConstants.MESSAGE, savedMessage);
            context.sendBroadcast(smilRecievedIntent);
        }
        catch (UnknownHostException e)
        {
            Log.e("ClientHandler", "Could not reach host server", e);
        }
        catch (IOException e)
        {
            Log.e("ClientHandler", "I/O error occured while working with resources", e);
        }
    }


    /**
     * Utility method to manage resource commands
     * 
     * @param connection connection to the message server
     */
    private void handleCommand(Socket connection)
    {
        if (command.equals("end"))
        {
            finished = true;
        }
        else if (command.equals("resource"))
        {
            connectionStatus = ConnectionStatus.RECIEVING_RESOURCE_DETAILS;
            readInput(connection);
            connectionStatus = ConnectionStatus.RECIEVING_RESOURCE_DATA;
            readInput(connection);
        }
    }


    /**
     * Saves a XML message to the database
     * 
     * @param sender sender of the message
     * @param messageText xml content of the message
     * @return the database message that was saved
     */
    private Message saveMessage(String sender, String messageText)
    {
        Message message = new Message(messageText, FolderType.INBOX, false, sender);
        message.save(context);
        return message;
    }


    @Override
    /**
     * Runs the client handler with the intended action
     */
    public void run()
    {
        if (action == MessageAction.SEND)
        {
            send();
        }
        else if (action == MessageAction.RECIEVE)
        {
            recieve();
        }
    }
}
